// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text;

namespace ResultsOfTGenerator;

public class Program
{
    private const int TYPE_ARG_COUNT = 6;

    public static void Main(string[] args)
    {
        // By default we assume we're being run in the context of the <repo>/src/Http/Http.Results/tools/ResultsOfTGenerator directory
        var pwd = Directory.GetCurrentDirectory();
        var classTargetFilePath = Path.Combine(pwd, "..", "..", "src", "ResultsOfT.Generated.cs");
        var testsTargetFilePath = Path.Combine(pwd, "..", "..", "test", "ResultsOfTTests.Generated.cs");

        if (args.Length > 0)
        {
            if (args.Length != 2)
            {
                throw new ArgumentException("Invalid number of args specified. Must specify both class file path and test file path if args are passed.");
            }

            classTargetFilePath = args[0];
            testsTargetFilePath = args[1];
        }

        GenerateClassFile(classTargetFilePath, TYPE_ARG_COUNT, args.Length == 0);

        GenerateTestFiles(testsTargetFilePath, TYPE_ARG_COUNT, args.Length == 0);
    }

    public static void Run(string classFilePath, string testsFilePath)
    {
        GenerateClassFile(classFilePath, TYPE_ARG_COUNT, false);

        GenerateTestFiles(testsFilePath, TYPE_ARG_COUNT, false);
    }

    static void GenerateClassFile(string classFilePath, int typeArgCount, bool interactive = true)
    {
        Console.WriteLine($"Will generate class file at {classFilePath}");

        if (interactive)
        {
            Console.WriteLine("Press any key to continue or Ctrl-C to cancel");
            Console.ReadKey();
        }

        using var writer = new StreamWriter(classFilePath, append: false);

        // File header
        writer.WriteLine("// Licensed to the .NET Foundation under one or more agreements.");
        writer.WriteLine("// The .NET Foundation licenses this file to you under the MIT license.");
        writer.WriteLine();
        writer.WriteLine("// This file is generated by a tool. See: src/Http/Http.Results/tools/ResultsOfTGenerator");
        writer.WriteLine();

        // Usings
        writer.WriteLine("using System.Diagnostics.CodeAnalysis;");
        writer.WriteLine("using System.Reflection;");
        writer.WriteLine("using Microsoft.AspNetCore.Builder;");
        writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;");
        writer.WriteLine();

        // Namespace
        writer.WriteLine("namespace Microsoft.AspNetCore.Http.HttpResults;");
        writer.WriteLine();

        // Skip 1 as we don't have a Results<TResult1> class
        for (int i = 2; i <= typeArgCount; i++)
        {
            // Class summary doc
            writer.WriteLine("/// <summary>");
            writer.WriteLine($"/// An <see cref=\"IResult\"/> that could be one of {i.ToWords()} different <see cref=\"IResult\"/> types. On execution will");
            writer.WriteLine("/// execute the underlying <see cref=\"IResult\"/> instance that was actually returned by the HTTP endpoint.");
            writer.WriteLine("/// </summary>");

            // Class remarks doc
            writer.WriteLine("/// <remarks>");
            writer.WriteLine("/// An instance of this type cannot be created explicitly. Use the implicit cast operators to create an instance");
            writer.WriteLine("/// from an instance of one of the declared type arguments, e.g.");
            writer.WriteLine("/// <code>Results&lt;Ok, BadRequest&gt; result = TypedResults.Ok();</code>");
            writer.WriteLine("/// </remarks>");

            // Type params docs
            for (int j = 1; j <= i; j++)
            {
                writer.WriteLine(@$"/// <typeparam name=""TResult{j}"">The {j.ToOrdinalWords()} result type.</typeparam>");
            }

            // Class declaration
            writer.Write($"public sealed class Results<");

            // Type args
            for (int j = 1; j <= i; j++)
            {
                writer.Write($"[DynamicallyAccessedMembers(ResultsOfTHelper.RequireMethods)] TResult{j}");
                if (j != i)
                {
                    writer.Write(", ");
                }
            }
            writer.Write(">");

            // Interfaces
            writer.WriteLine(" : IResult, INestedHttpResult, IEndpointMetadataProvider");

            // Type arg constraints
            for (int j = 1; j <= i; j++)
            {
                writer.WriteIndent($"where TResult{j} : IResult");
                if (j != i)
                {
                    writer.WriteLine();
                }
            }
            writer.WriteLine();
            writer.WriteLine("{");

            // Ctor
            writer.WriteIndentedLine("// Use implicit cast operators to create an instance");
            writer.WriteIndentedLine($"private Results(IResult activeResult)");
            writer.WriteIndentedLine("{");
            writer.WriteIndentedLine(2, "Result = activeResult;");
            writer.WriteIndentedLine("}");
            writer.WriteLine();

            // Result property
            writer.WriteIndentedLine("/// <summary>");
            writer.WriteIndentedLine($"/// Gets the actual <see cref=\"IResult\"/> returned by the <see cref=\"Endpoint\"/> route handler delegate.");
            writer.WriteIndentedLine("/// </summary>");
            writer.WriteIndentedLine("public IResult Result { get; }");
            writer.WriteLine();

            // ExecuteAsync method
            writer.WriteIndentedLine("/// <inheritdoc/>");
            writer.WriteIndentedLine("public Task ExecuteAsync(HttpContext httpContext)");
            writer.WriteIndentedLine("{");
            writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(httpContext);");
            writer.WriteLine();
            writer.WriteIndentedLine(2, "if (Result is null)");
            writer.WriteIndentedLine(2, "{");
            writer.WriteIndentedLine(3, "throw new InvalidOperationException(\"The IResult assigned to the Result property must not be null.\");");
            writer.WriteIndentedLine(2, "}");
            writer.WriteLine();
            writer.WriteIndentedLine(2, "return Result.ExecuteAsync(httpContext);");
            writer.WriteIndentedLine("}");
            writer.WriteLine();

            // Implicit converter operators
            var sb = new StringBuilder();
            for (int j = 1; j <= i; j++)
            {
                sb.AppendFormat(CultureInfo.InvariantCulture, "TResult{0}", j);
                if (j != i)
                {
                    sb.Append(", ");
                }
            }
            var typeArgsList = sb.ToString();

            for (int j = 1; j <= i; j++)
            {
                writer.WriteIndentedLine("/// <summary>");
                writer.WriteIndentedLine($"/// Converts the <typeparamref name=\"TResult{j}\"/> to a <see cref=\"Results{{{typeArgsList}}}\" />.");
                writer.WriteIndentedLine("/// </summary>");
                writer.WriteIndentedLine("/// <param name=\"result\">The result.</param>");
                writer.WriteIndentedLine($"public static implicit operator Results<{typeArgsList}>(TResult{j} result) => new(result);");

                if (i != j)
                {
                    writer.WriteLine();
                }
            }
            writer.WriteLine();

            // IEndpointMetadataProvider.PopulateMetadata
            writer.WriteIndentedLine("/// <inheritdoc/>");
            writer.WriteIndentedLine("static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)");
            writer.WriteIndentedLine("{");
            writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(method);");
            writer.WriteIndentedLine(2, "ArgumentNullException.ThrowIfNull(builder);");
            writer.WriteLine();
            for (int j = 1; j <= i; j++)
            {
                writer.WriteIndentedLine(2, $"ResultsOfTHelper.PopulateMetadataIfTargetIsIEndpointMetadataProvider<TResult{j}>(method, builder);");
            }
            writer.WriteIndentedLine("}");

            // Class end
            writer.WriteLine("}");

            if (i != typeArgCount)
            {
                writer.WriteLine();
            }
        }

        writer.Flush();
        writer.Close();

        var file = new FileInfo(classFilePath);

        if (!file.Exists)
        {
            throw new FileNotFoundException(classFilePath);
        }

        Console.WriteLine();
        Console.WriteLine($"{file.Length:N0} bytes written to {file.FullName} successfully!");
        Console.WriteLine();
    }

    static void GenerateTestFiles(string testFilePath, int typeArgCount, bool interactive = true)
    {
        Console.WriteLine($"Will generate tests file at {testFilePath}");

        if (interactive)
        {
            Console.WriteLine("Press any key to continue or Ctrl-C to cancel");
            Console.ReadKey();
        }

        using var writer = new StreamWriter(testFilePath, append: false);

        // File header
        writer.WriteLine("// Licensed to the .NET Foundation under one or more agreements.");
        writer.WriteLine("// The .NET Foundation licenses this file to you under the MIT license.");
        writer.WriteLine();
        writer.WriteLine("// This file is generated by a tool. See: src/Http/Http.Results/tools/ResultsOfTGenerator");

        // Using statements
        writer.WriteLine("using System.Reflection;");
        writer.WriteLine("using Microsoft.AspNetCore.Builder;");
        writer.WriteLine("using Microsoft.AspNetCore.Http.HttpResults;");
        writer.WriteLine("using Microsoft.AspNetCore.Http.Metadata;");
        writer.WriteLine("using Microsoft.AspNetCore.Routing;");
        writer.WriteLine("using Microsoft.AspNetCore.Routing.Patterns;");
        writer.WriteLine();

        // Namespace
        writer.WriteLine("namespace Microsoft.AspNetCore.Http.Result;");
        writer.WriteLine();

        // Class declaration
        writer.WriteLine($"public partial class ResultsOfTTests");
        writer.WriteLine("{");

        for (int i = 1; i <= typeArgCount; i++)
        {
            // Skip first as we don't have a Results<TResult1> class
            if (i == 1)
            {
                continue;
            }

            GenerateTest_Result_IsAssignedResult(writer, i);
            GenerateTest_ExecuteResult_ExecutesAssignedResult(writer, i);
            GenerateTest_Throws_ArgumentNullException_WhenHttpContextIsNull(writer, i);
            GenerateTest_Throws_InvalidOperationException_WhenResultIsNull(writer, i);
            GenerateTest_AcceptsIResult_AsAnyTypeArg(writer, i);
            GenerateTest_AcceptsNestedResultsOfT_AsAnyTypeArg(writer, i);
            GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImplementIEndpointMetadataProvider(writer, i);
            GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderIsNull(writer, i);
        }

        Generate_ChecksumResultClass(writer);

        // CustomResult classes
        writer.WriteLine();
        for (int i = 1; i <= typeArgCount + 1; i++)
        {
            Generate_ChecksumResultClass(writer, i);
            Generate_ProvidesMetadataResultClass(writer, i);

            if (i != typeArgCount)
            {
                writer.WriteLine();
            }
        }

        // End test class
        writer.WriteLine("}");

        writer.Flush();
        writer.Close();

        var file = new FileInfo(testFilePath);

        if (!file.Exists)
        {
            throw new FileNotFoundException(testFilePath);
        }

        Console.WriteLine();
        Console.WriteLine($"{file.Length:N0} bytes written to {file.FullName} successfully!");
    }

    static void GenerateTest_Result_IsAssignedResult(StreamWriter writer, int typeArgNumber)
    {
        //[Theory]
        //[InlineData(1, typeof(ChecksumResult1))]
        //[InlineData(2, typeof(ChecksumResult2))]
        //public void ResultsOfTResult1TResult2_Result_IsAssignedResult(int input, Type expectedResultType)
        //{
        //    // Arrange
        //    Results<CustomResult1, CustomResult2> MyApi(int id)
        //    {
        //        return id switch
        //        {
        //            1 => new CustomResult1(),
        //            _ => new CustomResult2()
        //        };
        //    }

        //    // Act
        //    var result = MyApi(input);

        //    // Assert
        //    Assert.IsType(expectedResultType, result.Result);
        //}

        // Attributes
        writer.WriteIndentedLine("[Theory]");

        // InlineData
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.WriteIndentedLine($"[InlineData({j}, typeof(ChecksumResult{j}))]");
        }

        // Method
        writer.WriteIndent(1, "public void ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_Result_IsAssignedResult(int input, Type expectedResultType)");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndent(2, "Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ChecksumResult{j}");
            if (typeArgNumber != j)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine("> MyApi(int id)");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return id switch");
        writer.WriteIndentedLine(3, "{");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            if (j != typeArgNumber)
            {
                writer.WriteIndentedLine(4, $"{j} => new ChecksumResult{j}(),");
            }
            else
            {
                writer.WriteIndentedLine(4, $"_ => new ChecksumResult{j}()");
            }
        }
        writer.WriteIndentedLine(3, "};");
        writer.WriteIndentedLine(2, "}");
        writer.WriteLine();

        // Act
        writer.WriteIndentedLine(2, "// Act");
        writer.WriteIndentedLine(2, "var result = MyApi(input);");
        writer.WriteLine();

        // Assert
        writer.WriteIndentedLine(2, "// Assert");
        writer.WriteIndentedLine(2, "Assert.IsType(expectedResultType, result.Result);");

        // End of method
        writer.WriteIndentedLine("}");
        writer.WriteLine();
    }

    static void GenerateTest_ExecuteResult_ExecutesAssignedResult(StreamWriter writer, int typeArgNumber)
    {
        //[Theory]
        //[InlineData(1, 1)]
        //[InlineData(2, 2)]
        //[InlineData(-1, null)]
        //public async Task ResultsOfTResult1TResult2_ExecuteResult_ExecutesAssignedResult(int input, object expected)
        //{
        //    // Arrange
        //    Results<ChecksumResult1, ChecksumResult2, NoContent> MyApi(int checksum)
        //    {
        //        return checksum switch
        //        {
        //            1 => new ChecksumResult1(checksum),
        //            2 => new ChecksumResult2(checksum),
        //            _ => (NoContent)Results.NoContent()
        //        };
        //    }
        //    var httpContext = GetHttpContext();

        //    // Act
        //    var result = MyApi(input);
        //    await result.ExecuteAsync(httpContext);

        //    // Assert
        //    Assert.Equal(expected, httpContext.Items[nameof(ChecksumResult.Checksum)]);
        //}

        // Attributes
        writer.WriteIndentedLine("[Theory]");

        // InlineData
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.WriteIndentedLine($"[InlineData({j})]");
        }

        // Method
        // public void ResultsOfTResult1TResult2_ExecuteResult_ExecutesAssignedResult(int input, object expected)
        writer.WriteIndent(1, "public async Task ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_ExecuteResult_ExecutesAssignedResult(int input)");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndent(2, "Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ChecksumResult{j}");
            if (typeArgNumber != j)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine("> MyApi(int checksum)");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return checksum switch");
        writer.WriteIndentedLine(3, "{");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            if (j < typeArgNumber)
            {
                writer.WriteIndentedLine(4, $"{j} => new ChecksumResult{j}(checksum),");
            }
            else
            {
                writer.WriteIndentedLine(4, $"_ => new ChecksumResult{j}(checksum)");
            }
        }
        writer.WriteIndentedLine(3, "};");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(2, "var httpContext = GetHttpContext();");
        writer.WriteLine();

        // Act
        writer.WriteIndentedLine(2, "// Act");
        writer.WriteIndentedLine(2, "var result = MyApi(input);");
        writer.WriteIndentedLine(2, "await result.ExecuteAsync(httpContext);");
        writer.WriteLine();

        // Assert
        writer.WriteIndentedLine(2, "// Assert");
        writer.WriteIndentedLine(2, "Assert.Equal(input, httpContext.Items[nameof(ChecksumResult.Checksum)]);");

        // End of method
        writer.WriteIndentedLine("}");
        writer.WriteLine();
    }

    static void GenerateTest_Throws_ArgumentNullException_WhenHttpContextIsNull(StreamWriter writer, int typeArgNumber)
    {
        //[Fact]
        //public async Task ResultsOfTResult1TResult2_Throws_ArgumentNullException_WhenHttpContextIsNull()
        //{
        //    // Arrange
        //    Results<ChecksumResult1, NoContent> MyApi()
        //    {
        //        return new ChecksumResult1(1);
        //    }
        //    HttpContext httpContext = null;

        //    // Act & Assert
        //    var result = MyApi();

        //    await Assert.ThrowsAsync<ArgumentNullException>(async () =>
        //    {
        //        await result.ExecuteAsync(httpContext);
        //    });
        //}

        // Attributes
        writer.WriteIndentedLine("[Fact]");

        // Start method
        writer.WriteIndent(1, "public async Task ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_Throws_ArgumentNullException_WhenHttpContextIsNull()");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndentedLine(2, "Results<ChecksumResult1, NoContent> MyApi()");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return new ChecksumResult1(1);");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(2, "HttpContext httpContext = null;");
        writer.WriteLine();

        // Act & Assert
        writer.WriteIndentedLine(2, "// Act & Assert");
        writer.WriteIndentedLine(2, "var result = MyApi();");
        writer.WriteLine();

        writer.WriteIndentedLine(2, "await Assert.ThrowsAsync<ArgumentNullException>(async () =>");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "await result.ExecuteAsync(httpContext);");
        writer.WriteIndentedLine(2, "});");

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void GenerateTest_Throws_InvalidOperationException_WhenResultIsNull(StreamWriter writer, int typeArgNumber)
    {
        //[Fact]
        //public async Task ResultsOfTResult1TResult2_Throws_InvalidOperationException_WhenResultIsNull()
        //{
        //    // Arrange
        //    Results<ChecksumResult1, NoContent> MyApi()
        //    {
        //        return (ChecksumResult1)null;
        //    }
        //    var httpContext = GetHttpContext();

        //    // Act & Assert
        //    var result = MyApi();

        //    await Assert.ThrowsAsync<InvalidOperationException>(async () =>
        //    {
        //        await result.ExecuteAsync(httpContext);
        //    });
        //}

        // Attributes
        writer.WriteIndentedLine("[Fact]");

        // Start method
        writer.WriteIndent(1, "public async Task ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_Throws_InvalidOperationException_WhenResultIsNull()");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndentedLine(2, "Results<ChecksumResult1, NoContent> MyApi()");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return (ChecksumResult1)null;");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(2, "var httpContext = GetHttpContext();");
        writer.WriteLine();

        // Act & Assert
        writer.WriteIndentedLine(2, "// Act & Assert");
        writer.WriteIndentedLine(2, "var result = MyApi();");
        writer.WriteLine();

        writer.WriteIndentedLine(2, "await Assert.ThrowsAsync<InvalidOperationException>(async () =>");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "await result.ExecuteAsync(httpContext);");
        writer.WriteIndentedLine(2, "});");

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void GenerateTest_AcceptsIResult_AsAnyTypeArg(StreamWriter writer, int typeArgCount)
    {
        for (int i = 1; i <= typeArgCount; i++)
        {
            GenerateTest_AcceptsIResult_AsNthTypeArg(writer, typeArgCount, i);
        }
    }

    static void GenerateTest_AcceptsIResult_AsNthTypeArg(StreamWriter writer, int typeArgCount, int typeArgNumber)
    {
        //[Theory]
        //[InlineData(1, typeof(ChecksumResult1))]
        //[InlineData(2, typeof(ChecksumResult2))]
        //public async Task ResultsOfTResult1TResult2_AcceptsIResult_AsFirstTypeArg(int input, Type expectedResultType)
        //{
        //    // Arrange
        //    Results<IResult, ChecksumResult2> MyApi(int id)
        //    {
        //        return id switch
        //        {
        //            1 => new ChecksumResult1(1),
        //            _ => new ChecksumResult2(2)
        //        };
        //    }
        //    var httpContext = GetHttpContext();

        //    // Act
        //    var result = MyApi(input);
        //    await result.ExecuteAsync(httpContext);

        //    // Assert
        //    Assert.IsType(expectedResultType, result.Result);
        //    Assert.Equal(input, httpContext.Items[nameof(ChecksumResult.Checksum)]);
        //}

        // Attributes
        writer.WriteIndentedLine("[Theory]");

        // InlineData
        for (int j = 1; j <= typeArgCount; j++)
        {
            writer.WriteIndentedLine($"[InlineData({j}, typeof(ChecksumResult{j}))]");
        }

        // Start method
        writer.WriteIndent(1, "public async Task ResultsOf");
        for (int j = 1; j <= typeArgCount; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine($"_AcceptsIResult_As{typeArgNumber.ToOrdinalWords().TitleCase()}TypeArg(int input, Type expectedResultType)");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndent(2, "Results<");
        for (int j = 1; j <= typeArgCount; j++)
        {
            if (j == typeArgNumber)
            {
                writer.Write("IResult");
            }
            else
            {
                writer.Write($"ChecksumResult{j}");
            }

            if (j < typeArgCount)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine("> MyApi(int id)");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return id switch");
        writer.WriteIndentedLine(3, "{");
        for (int j = 1; j <= typeArgCount; j++)
        {
            if (j < typeArgCount)
            {
                writer.WriteIndentedLine(4, $"{j} => new ChecksumResult{j}({j}),");
            }
            else
            {
                writer.WriteIndentedLine(4, $"_ => new ChecksumResult{j}({j})");
            }
        }
        writer.WriteIndentedLine(3, "};");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(2, "var httpContext = GetHttpContext();");
        writer.WriteLine();

        // Act
        writer.WriteIndentedLine(2, "// Act");
        writer.WriteIndentedLine(2, "var result = MyApi(input);");
        writer.WriteIndentedLine(2, "await result.ExecuteAsync(httpContext);");
        writer.WriteLine();

        // Assert
        writer.WriteIndentedLine(2, "// Assert");
        writer.WriteIndentedLine(2, "Assert.IsType(expectedResultType, result.Result);");
        writer.WriteIndentedLine(2, "Assert.Equal(input, httpContext.Items[nameof(ChecksumResult.Checksum)]);");

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void GenerateTest_AcceptsNestedResultsOfT_AsAnyTypeArg(StreamWriter writer, int typeArgCount)
    {
        for (int i = 1; i <= typeArgCount; i++)
        {
            GenerateTest_AcceptsNestedResultsOfT_AsNthTypeArg(writer, typeArgCount, i);
        }
    }

    static void GenerateTest_AcceptsNestedResultsOfT_AsNthTypeArg(StreamWriter writer, int typeArgCount, int typeArgNumber)
    {
        //[Theory]
        //[InlineData(1, typeof(Results<ChecksumResult1, ChecksumResult2>))]
        //[InlineData(2, typeof(Results<ChecksumResult1, ChecksumResult2>))]
        //[InlineData(3, typeof(ChecksumResult3))]
        //public async Task ResultsOfTResult1TResult2_AcceptsNestedResultsOfT_AsFirstTypeArg(int input, Type expectedResultType)
        //{
        //    // Arrange
        //    Results<Results<ChecksumResult1, ChecksumResult2>, ChecksumResult3> MyApi(int id)
        //    {
        //        return id switch
        //        {
        //            1 => (Results<ChecksumResult1, ChecksumResult2>)new ChecksumResult1(1),
        //            2 => (Results<ChecksumResult1, ChecksumResult2>)new ChecksumResult2(2),
        //            _ => new ChecksumResult3(3)
        //        };
        //    }
        //    var httpContext = GetHttpContext();

        //    // Act
        //    var result = MyApi(input);
        //    await result.ExecuteAsync(httpContext);

        //    // Assert
        //    Assert.IsType(expectedResultType, result.Result);
        //    Assert.Equal(input, httpContext.Items[nameof(ChecksumResult.Checksum)]);
        //}

        var sb = new StringBuilder("Results<");
        for (int j = 1; j <= typeArgCount; j++)
        {
            sb.Append(CultureInfo.InvariantCulture, $"ChecksumResult{j}");

            if (j < typeArgCount)
            {
                sb.Append(", ");
            }
        }
        sb.Append('>');
        var nestedResultTypeName = sb.ToString();

        // Attributes
        writer.WriteIndentedLine("[Theory]");

        // InlineData
        for (int j = 1; j <= typeArgCount + 1; j++)
        {
            if (j <= typeArgCount)
            {
                writer.WriteIndentedLine($"[InlineData({j}, typeof({nestedResultTypeName}))]");
            }
            else
            {
                writer.WriteIndentedLine($"[InlineData({j}, typeof(ChecksumResult{j}))]");
            }
        }

        // Start method
        writer.WriteIndent(1, "public async Task ResultsOf");
        for (int j = 1; j <= typeArgCount; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine($"_AcceptsNestedResultsOfT_As{typeArgNumber.ToOrdinalWords().TitleCase()}TypeArg(int input, Type expectedResultType)");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndent(2, "Results<");
        writer.WriteLine($"{nestedResultTypeName}, ChecksumResult{typeArgCount + 1}> MyApi(int id)");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, "return id switch");
        writer.WriteIndentedLine(3, "{");
        for (int j = 1; j <= typeArgCount; j++)
        {
            writer.WriteIndentedLine(4, $"{j} => ({nestedResultTypeName})new ChecksumResult{j}({j}),");
        }
        writer.WriteIndentedLine(4, $"_ => new ChecksumResult{typeArgCount + 1}({typeArgCount + 1})");
        writer.WriteIndentedLine(3, "};");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(2, "var httpContext = GetHttpContext();");
        writer.WriteLine();

        // Act
        writer.WriteIndentedLine(2, "// Act");
        writer.WriteIndentedLine(2, "var result = MyApi(input);");
        writer.WriteIndentedLine(2, "await result.ExecuteAsync(httpContext);");
        writer.WriteLine();

        // Assert
        writer.WriteIndentedLine(2, "// Assert");
        writer.WriteIndentedLine(2, "Assert.IsType(expectedResultType, result.Result);");
        writer.WriteIndentedLine(2, "Assert.Equal(input, httpContext.Items[nameof(ChecksumResult.Checksum)]);");

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void GenerateTest_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImplementIEndpointMetadataProvider(StreamWriter writer, int typeArgNumber)
    {
        //[Fact]
        //public void ResultsOfTResult1TResult2_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImplementIEndpointMetadataProvider()
        //{
        //    // Arrange
        //    Results<ProvidesMetadataResult1, ProvidesMetadataResult2> MyApi() { throw new NotImplementedException(); }
        //    var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0);

        //    // Act
        //    PopulateMetadata<Results<ProvidesMetadataResult1, ProvidesMetadataResult2>>(((Delegate)MyApi).GetMethodInfo(), builder);

        //    // Assert
        //    Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult1) });
        //    Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata { SourceTypeName: nameof(ProvidesMetadataResult2) });
        //}

        // Attributes
        writer.WriteIndentedLine("[Fact]");

        // Start method
        writer.WriteIndent(1, "public void ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_PopulateMetadata_PopulatesMetadataFromTypeArgsThatImplementIEndpointMetadataProvider()");
        writer.WriteIndentedLine("{");

        // Arrange
        writer.WriteIndentedLine(2, "// Arrange");
        writer.WriteIndent(2, "Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ProvidesMetadataResult{j}");

            if (j != typeArgNumber)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine("> MyApi() { throw new NotImplementedException(); }");
        writer.WriteIndentedLine(2, @"var builder = new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0);");
        writer.WriteLine();

        // Act
        writer.WriteIndentedLine(2, "// Act");
        writer.WriteIndent(2, "PopulateMetadata<Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ProvidesMetadataResult{j}");

            if (j != typeArgNumber)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine(">>(((Delegate)MyApi).GetMethodInfo(), builder);");
        writer.WriteLine();

        // Assert
        writer.WriteIndentedLine(2, "// Assert");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.WriteIndentedLine(2, $"Assert.Contains(builder.Metadata, m => m is ResultTypeProvidedMetadata {{ SourceTypeName: nameof(ProvidesMetadataResult{j}) }});");
        }

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void GenerateTest_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderIsNull(StreamWriter writer, int typeArgNumber)
    {
        //[Fact]
        //public void ResultsOfTResult1TResult2_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull()
        //{
        //    // Act & Assert
        //    Assert.Throws<ArgumentNullException>("method", () => PopulateMetadata<Results<ProvidesMetadataResult1, ProvidesMetadataResult2>>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse("/"), order: 0)));
        //    Assert.Throws<ArgumentNullException>("builder", () => PopulateMetadata<Results<ProvidesMetadataResult1, ProvidesMetadataResult2>>(((Delegate)MyApi).GetMethodInfo(), null));
        //}

        // Attributes
        writer.WriteIndentedLine("[Fact]");

        // Start method
        writer.WriteIndent(1, "public void ResultsOf");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"TResult{j}");
        }
        writer.WriteLine("_PopulateMetadata_Throws_ArgumentNullException_WhenMethodOrBuilderAreNull()");
        writer.WriteIndentedLine("{");

        // Act & Assert
        writer.WriteIndentedLine(2, "// Act & Assert");
        writer.WriteIndent(2, "Assert.Throws<ArgumentNullException>(\"method\", () => PopulateMetadata<Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ProvidesMetadataResult{j}");

            if (j != typeArgNumber)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine(@">>(null, new RouteEndpointBuilder(requestDelegate: null, RoutePatternFactory.Parse(""/""), order: 0)));");

        writer.WriteIndent(2, "Assert.Throws<ArgumentNullException>(\"builder\", () => PopulateMetadata<Results<");
        for (int j = 1; j <= typeArgNumber; j++)
        {
            writer.Write($"ProvidesMetadataResult{j}");

            if (j != typeArgNumber)
            {
                writer.Write(", ");
            }
        }
        writer.WriteLine(">>(((Delegate)GeneratedCodeIsUpToDate).GetMethodInfo(), null));");

        // Close method
        writer.WriteIndentedLine(1, "}");
        writer.WriteLine();
    }

    static void Generate_ChecksumResultClass(StreamWriter writer, int typeArgNumber = -1)
    {
        if (typeArgNumber <= 0)
        {
            writer.WriteIndentedLine(1, "abstract class ChecksumResult : IResult");
            writer.WriteIndentedLine(1, "{");
            writer.WriteIndentedLine(2, "public ChecksumResult(int checksum = 0)");
            writer.WriteIndentedLine(2, "{");
            writer.WriteIndentedLine(3, "Checksum = checksum;");
            writer.WriteIndentedLine(2, "}");
            writer.WriteLine();
            writer.WriteIndentedLine(2, "public int Checksum { get; }");
            writer.WriteLine();
            writer.WriteIndentedLine(2, "public Task ExecuteAsync(HttpContext httpContext)");
            writer.WriteIndentedLine(2, "{");
            writer.WriteIndentedLine(3, "httpContext.Items[nameof(ChecksumResult.Checksum)] = Checksum;");
            writer.WriteIndentedLine(3, "return Task.CompletedTask;");
            writer.WriteIndentedLine(2, "}");
            writer.WriteIndentedLine(1, "}");
        }
        else
        {
            // ChecksumResult class
            //class ChecksumResult1 : ChecksumResult
            //{
            //    public ChecksumResult1(int checksum = 0) : base(checksum) { }
            //}
            writer.WriteIndentedLine(1, $"class ChecksumResult{typeArgNumber} : ChecksumResult");
            writer.WriteIndentedLine(1, "{");
            writer.WriteIndentedLine(2, $"public ChecksumResult{typeArgNumber}(int checksum = 0) : base(checksum) {{ }}");
            writer.WriteIndentedLine(1, "}");
        }
    }

    static void Generate_ProvidesMetadataResultClass(StreamWriter writer, int typeArgNumber)
    {
        //private sealed class ProvidesMetadataResult1 : IResult, IEndpointMetadataProvider
        //{
        //    public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask;

        //    public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
        //    {
        //        builder.Metadata.Add(new ResultTypeProvidedMetadata { SourceTypeName = nameof(ProvidesMetadataResult1) });
        //    }
        //}
        writer.WriteIndentedLine(1, $"class ProvidesMetadataResult{typeArgNumber} : IResult, IEndpointMetadataProvider");
        writer.WriteIndentedLine(1, "{");
        writer.WriteIndentedLine(2, "public Task ExecuteAsync(HttpContext httpContext) => Task.CompletedTask;");
        writer.WriteLine();
        writer.WriteIndentedLine(2, "public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)");
        writer.WriteIndentedLine(2, "{");
        writer.WriteIndentedLine(3, $"builder.Metadata.Add(new ResultTypeProvidedMetadata {{ SourceTypeName = nameof(ProvidesMetadataResult{typeArgNumber}) }});");
        writer.WriteIndentedLine(2, "}");
        writer.WriteIndentedLine(1, "}");
    }
}
public static class StringExtensions
{
    public static void WriteIndent(this StreamWriter writer, string? value = null)
    {
        WriteIndent(writer, 1, value);
    }

    public static void WriteIndent(this StreamWriter writer, int count, string? value = null)
    {
        for (var i = 1; i <= count; i++)
        {
            writer.Write("    ");
        }

        if (value != null)
        {
            writer.Write(value);
        }
    }

    public static void WriteIndentedLine(this StreamWriter writer, string? value)
    {
        WriteIndentedLine(writer, 1, value);
    }

    public static void WriteIndentedLine(this StreamWriter writer, int count, string? value)
    {
        WriteIndent(writer, count, value);
        writer.WriteLine();
    }

    public static string ToWords(this int number) => number switch
    {
        1 => "one",
        2 => "two",
        3 => "three",
        4 => "four",
        5 => "five",
        6 => "six",
        7 => "seven",
        8 => "eight",
        9 => "nine",
        10 => "ten",
        11 => "eleven",
        12 => "twelve",
        13 => "thirteen",
        14 => "fourteen",
        15 => "fifteen",
        16 => "sixteen",
        17 => "seventeen",
        18 => "eighteen",
        19 => "nineteen",
        20 => "twenty",
        _ => throw new NotImplementedException("Add more numbers")
    };

    public static string ToOrdinalWords(this int number) => number switch
    {
        1 => "first",
        2 => "second",
        3 => "third",
        4 => "fourth",
        5 => "fifth",
        6 => "sixth",
        7 => "seventh",
        8 => "eighth",
        9 => "ninth",
        10 => "tenth",
        11 => "eleventh",
        12 => "twelfth",
        13 => "thirteenth",
        14 => "fourteenth",
        15 => "fifteenth",
        16 => "sixteenth",
        17 => "seventeenth",
        18 => "eighteenth",
        19 => "nineteenth",
        20 => "twentieth",
        _ => throw new NotImplementedException("Add more numbers")
    };

    public static string TitleCase(this string value) => string.Create(value.Length, value, (c, s) =>
    {
        var origValueSpan = s.AsSpan();
        c[0] = char.ToUpper(origValueSpan[0], CultureInfo.InvariantCulture);
        origValueSpan[1..].TryCopyTo(c[1..]);
    });
}
